6장. 메소드

학습목표 : 메소드 설계의 다양한 측면을 알아본다.

항목 23 : 인자의 유효성을 검사하라

1. 대부분의 메소드와 생성자는 인자로 받을 수 있는 값에 제한이 있다.

-제약 조건들은 반드시 문서화 되어야 한다.
-유효성 검사를 통해 인자가 유효하지 않을 때 적절한 예외를 발생시키면, 빠르고 깔끔하게 메소드를 종료할 수 있다.
-public 메소드의 경우 Javadoc 의 @throws 태그를 써서 인자값에 제약조건을 어겼을 때 예외를 문서화 해야한다.


/**
 * (this mod m)의 값을 가지는 BigInteger를 리턴한다.
 * 이 메소드는 항상 음수가 아닌 BigInteger를 리턴한다는 점이 
 * remainder 메소드와 다르다
 * 
 * @param m 나누는 수, 반드시 양수이다.
 * @return this BigInteger를 m으로 나눈 몫, 항상 양수이다.
 * @throw ArithmeticException m <= 0 일 때 발생한다.
 */

public BigInteger mod(BigInteger m){
    if(m.signum() <= 0)
        throw new ArithmeticException("Modulus not positive");
        
    ...// 실제 계산
}

-public이 아닌 메소드에서는 assertion을 써서 인자값을 검사해야 한다. (jdk 1.4 이상이면 assert명령어 사용)

  • assert : 자바 코드의 문장이나 디자인 전체를 검증할 때 사용
{code}
//=================================================================
// 메소드의 인자로 전달된 값을 미리 검증
//















-
public BigInteger mod(BigInteger m){
assert m.signum() <= 0;
...// 실제 계산
}
//=================================================================
{code}

-메소드 안에 직접 쓰이지 않지만 나중에 다른 곳에서 쓰기 위해 필드로 저장해 놓는 인자가 유효한지 검사하는 것은 매우 중요하다.
-클래스의 불변규칙을 어기는 객체가 생성되는 것을 막기 위해 생성자에 전달되는 인자의 유효성을 검사하는 것은 매우 중요하다.
-메소드 수행 과정에서 묵시적으로 인자에 대한 유효성을 검사할 때 잘못된 예외가 발생하는 경우가 있다.
-예외변환(exception translatioin) 구현패턴을 써서 자연스럽게 발생하는 예외를 메소드의 명세문서에 나온 예외로 바꿔야 한다.

-메소드나 생성자를 만들 때 인자에 어떤 제약 조건이 있는지 반드시 검토해야한다.
-제약 조건들을 메소드 본문의 시작부분에서 이 제약조건을 검사해야한다.

항목 24 : 필요한 경우 방어복사하라

  • 자바 프로그래밍 언어는 안전한 언어이다.
  • 안전한 언어라 해도, 클래스의 클라이언트가 불변규칙을 깨뜨리기 위해 최선을 다한다고 가정하고, 프로그래밍 할 때 항상 방어하는 자세를 가져야한다.
  • 클라이언트의 악의든 고의든 이상하게 행동할 수 있는 것까지 고려한 강건한 클래스를 만들어야 한다.
가짜 불편 클래스Period 인스턴스 공격하기
{code}
package efective;

import java.util.Date;

//=================================================================
//가짜 불변 클래스
//

















-
public final class Period{

//

















-
// Date객체 생성
//















-
private final Date start;
private final Date end;
//















-

/**

  • <pre>
  • @param start 시작일.
  • @param end 종료일. 반드시 시작일 이후이다.
  • @throws IllegalArgumentException 시작일이 종료일보다 늦을 때 발생한다.
  • @throws NullPointerException 시작일이나 종료일이 null 일 때 발생한다.
  • @author ssuk
  • </pre>
    */

//=================================================================
// 두 날짜 사이의 기간 표현
//

















-
public Period(Date start, Date end){

if(start.compareTo(end) > 0)
throw new IllegalArgumentException(start + "after" + end);
//

















-
this.start = start;
this.end = end;
}
//=================================================================

//=================================================================
// 시작일 리턴
//

















-
public Date start(){
return start;
}
//=================================================================

//=================================================================
// 종료일 리턴
//

















-
public Date end(){
return end;
}
//=================================================================

}

|

package efective;

import java.util.Date;

public class PeriodMain{

public static void main(String[] args){
//=================================================================
// 객체생성
//

















-
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
//















-
// Period 인스턴스의 내부를 공격하라 (수정하라)
//















-
end.setYear(78);
//=================================================================

//=================================================================
// Date Log Test
//

















-
System.out.println("=start=[" + start + "]");
System.out.println("=end =[" + end + "]");
//=================================================================
}
}


!p165.jpg!|
|▲ 이 클래스는 불변 클래스, 시작일이 종료일보다 앞설 수 밖에 없어 보인다. 
그러나 Date 클래스가 가변 클래스 이기에 불변규칙은 쉽게 깨진다. |▲ 위와 같은 공격으로 부터 보호하려면, 생성자에 전달되는 변경가능 인자들을 방어복사(defensive copy)해야 하고, 
원본대신에 복사본으로 Period 인스턴스를 만들어야 한다. |

h4.


||인자를 방어복사||또 다른 공격||
|

//=================================================================
// 인자를 방어복사한다.
//

















-
public Period(Date start, Date end){

this.start = new Date(start.getTime());
this.end = new Date(end.getTime());

if (this.start.compareTo(this.end)>0)
throw new IllegalArgumentException(start + "after" + end);

}
//=================================================================

|

//=================================================================
//Period 인스턴스에 대한 다른 공격
//

















-
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
//















-
p.end().setYear(78); //p를 수정한다.
//=================================================================

|
|인자의 유효성을 검사하기 전에 먼저 복사하고 나서 원본이 아닌 복사본의 유효성을 검사한다.
방어복사에 Date클래스의 Clone 메소드를 쓰지 않는다.
Date클래스는 final이 아니므로 Clone메소드가 리턴하는 객체가 정확히 java.util.Date 클래스의 인스턴스라고  
확신할 수 없다. 따라서 하위클래스가 생길 수 있는 방어복사에 Clone 메소드를 쓰지 말아야한다.|▲ Period 인스턴스 접근자 메소를 이용해 가변 객체참조 하기.|

h4.

||*Period 클래스는 완전한 불변 클래스가 되었다.*||
|

package efective;

import java.util.Date;

//=================================================================
//가짜 불변 클래스
//

















-
public final class Period{

//

















-
// Date객체 생성
//















-
private final Date start;
private final Date end;
//















-

/**

  • <pre>
  • @param start 시작일.
  • @param end 종료일. 반드시 시작일 이후이다.
  • @throws IllegalArgumentException 시작일이 종료일보다 늦을 때 발생한다.
  • @throws NullPointerException 시작일이나 종료일이 null 일 때 발생한다.
  • @author ssuk
  • </pre>
    */

//=================================================================
// 인자를 방어복사한다.
//

















-
public Period(Date start, Date end){

this.start = new Date(start.getTime());
this.end = new Date(end.getTime());

if (this.start.compareTo(this.end)>0)
throw new IllegalArgumentException(start + "after" + end);
}
//=================================================================

//=================================================================
// 시작일 내부 필드를 방어복사하여 리턴한다.
//

















-
public Date start(){
return (Date) start.clone();
}
//=================================================================

//=================================================================
// 종료일 내부 필드를 방어복사하여 리턴한다.
//

















-
public Date end(){
return (Date) end.clone();
}
//=================================================================
}

|


- 접근자에서는 생성자와 달리 clone  메소드를 써서 방어복사한다.
- Period 내부 Date객체는 java.util.Date 클래스의 인스턴스 라는 것이 확실하기 때문에 clone 메소드를 써도 문제가 없다.
- 메소드나 생성자 생성시, 클라이언트에서 제공하는 객체를 내부 구조에 저장해야 할 때, 그 객체가 변경가능한지 생각하고, 변경 가능하다면 객체를 방어복사해서 클래스 내부필드에 원본이 아닌 복사본을 저장해야 한다.
- 길이가 0이 아닌 모든 배열은 언제나 변경가능하다. 클라이언트에게 원본 배열 자체를 리턴하지 말고, 배열을 방어복사해서 리턴해야 한다.
- 방어복사와 같은 귀찮은 작업을 전혀 신경쓰지 않으려면, 불변 객체를 써야 한다.



h2. 항목 25 : 메소드 시그니처를 신중하게 설계하라
- API 설계에 대한 힌트를 모은 것이다.
- API를 배우기 쉽고, 쓰기 쉽고, 오류도 적게 발생하도록 만드는게 목적이다.

h4. 1. 메소드 이름을 신중하게 결정하라
-이름은 표준 작명규칙(standard naming convention)을 따라야한다.
-이해하기 쉽고, 같은 패키지에 있는 다른 클래스 혹은 인터페이스들과 일관성 있는 이름을 짓도록 해야한다.
-적절한 이름을 찾기 어렵다면  자바 플랫폼 라이브러리 API를 참고한다. 
-*Patrick Chan*의 *The Java Developers Almanac*도 매우 유용한 자료이다. [http://www.exampledepot.com/egs/]


h4. 2. 편리한 메소드를 제공하기 위해 너무 애쓰지 마라
-메소드가 너무 많으면 클래스를 배우고, 쓰고, 문서화하고, 시험하고, 관리하기 어려워진다.
-행위 하나하나에 대해 완벽하게 기능을 발휘하는 메소드를 제공히라.
-자주 사용하는 경우에만 속기(shorthand) 메소드 를 제공할지 고민하라.


h4. 3. 인자를 너무 많이 받지 마라.
-인자는 적을수록 좋다.
-대부분의 프로그래머는 인자 개수가 3개를 넘으면 기억하지 못한다.
-동일한 타입의 인자가 죽 이어져 있으면 훨씬 더 위험하다.

*인자의 길이를 줄이는 두가지 방법*
-첫째, 인자의 일부만 받는 여러 개의 메소드로 나누는 것이다.
(예 : java.util.List 인터페이스에는 처음과 마지막 요소의 인덱스를 찾는 메소드가 없다. 이런 메소드 구현시 모두 세개의 인자가 필요하다.
대신 indexOf, lastIndexOf를 조합하면 원하는 작업을 처리할 수 있다.)

-둘째, 인자들을 모아서 보관하는 헬퍼 클래스(Helper class)를 사용한다.
(예 : 카드케임 클래스를 만든다고 가정시 숫자와 무늬라는 두개의 인자를 받는 메소드가 계속 생길 것이다. 따라서 숫자와 무늬를 멤버로 가지는 
정적멤버클래스를 헬퍼클래스로 생성해두어 두 인자 대신에 이 헬퍼 클래스 타입으로 바꾸면
API와 내부 구조도 깔끔하게 정리된다.꾼다면 가 계속 나오는 것을 발견할 수 있다. )



h4. 4. 인자의 타입으로 클래스보다 인터페이스를 써라
-인자를 정의할 수 있는 적절한 인터페이스가 있다면, 이를 구현한 클래스 보다 인터페이스로 타입을 정하는 것이 좋다.
(예: Hashtable 을 인자로 받는 메소드를 만들 필요가 없다.)
-Map을 인자로 받는 메소드는 Hashtable, HashMap, TreeMap과 같은 현재 구현된 모든 종류의 Map구현체와 앞으로 구현될 모든 Map구현체를 쓸 수 있다. 

h4. 5. 기능객체(function object)는 신중하게 써라.


h2. 항목 26 : 메소드를 중복정의할 때는 신중하라
|

package effective;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

//=================================================================
// 틀린 구현 - 메소드 중복정의를 잘못 쓰고 있다.
//

















-
public class CollectionClassifier{

public static String classify(Set s){
return "Set";
}

public static String classify(List l){
return "List";
}

public static String classify(Collection c){
return "Unknown Collection";
}

public static void main(String[] args){

Collection[] tests = new Collection[]{
new HashSet(), // Set
new ArrayList(), // List
new HashMap().values() // Set 도 List도 아닌 것
};

for(int i=0; i<tests.length; i++){
//

















-
System.out.println("--classify(tests["i"])---["classify(tests[i])"]");
//















-
}
}
}
//=================================================================

|

//=================================================================
// 예상출력 결과
//

















-
Set
List
Unknown Collection
//=================================================================


!p172.jpg!
- 문제점 : classify 메소드가 중복정의 메소드(overloaded method)라는 것이다.
- 중복정의 메소드 중에서 어떤 것을 호출할지 고르는 것은 컴파일 시점에서 결정난다.
  때문에, 실행시점에 달라지는 타입은 중복정의 메소드를 고를 때 아무런 영향을 줄 수 없다.
|

*<중복정의 메소드 선택은 정적으로 결정, 재정의 메소드 선택은 동적으로 결정>*

||메소드를 재정의 하려면 하위클래스와 상위클래스의 메소드가 같은 시그니처를 가져야 한다.||클래스 A에 선언한 name 메소드를 클래스 B와 C가 재정의 한다.||
|

//================================================
// 재정의 메소드
//






















--
class A {
String name() { return "A" ; }
}

class B {
String name() { return "B" ; }
}

class C {
String name() { return "C" ; }
}

public class Overriding {
public static void main(String[] args) {
A[] tests = new A[] { new A(), new B(), new C()}

for(int i=0; i<tests.length; i++) {
System.out.println("---tests["i"].name()---["tests[i].name()"]");
}
}
}
//================================================

|

//================================================
// 예상출력 결과
//






















--
A
B
C
//================================================

//================================================
// 실제출력 결과
//






















--
A
B
C
//================================================

|

-부모 클래스와 자식 클래스에 동일한 메소드가 존재할 경우 : 
  컴파일러가 동일한 메소드가 중복되었다고 판단, 묵시적으로 부모 클래스의 메소드를 숨기고 자식 클래스의 메소드를 실행한다.


||CollectionClassifier 예제에서 classify 메소드를 하나로 합치고 instanceof 를 써서 타입을 검사해보자.||
|

package effective;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

//=================================================================
// 틀린 구현 - 메소드 중복정의를 잘못 쓰고 있다.
//

















-
public class CollectionClassifier{
public static String classify(Collection c){
return (c instanceof Set ? "Set" :
(c instanceof List ? "List" : "Unknown Collection"));
}

public static void main(String[] args){
Collection[] tests = new Collection[]{
new HashSet(), // Set
new ArrayList(), // List
new HashMap().values() // Set 도 List도 아닌 것
};

for(int i=0; i<tests.length; i++){
//

















-
System.out.println("--classify(tests["i"])---["classify(tests[i])"]");
//















-
}
}
}
//=================================================================

|
|!p172_1.jpg!|

-혼란을 주지 않고 메소드를 중복정의 하는 방법?
  가장 안전하고 보수적인 방법은 같은 개수의 인자를 갖는 중복정의 메소드를 만들지 않는 것이다.


//================================================
// ObjectOutputStream 클래스를 이용한 예.
//






















--
ObjectOutputStream 클래스 : 자바의 기본 자료형과 객체를 직렬화된 데이터로 저장하기 위해 사용되는 클래스

void writeBolean(boolean) // boolean 값을 출력한다.
void writeInt(int) // int 값을 출력한다.
void writeLong(long) // long 값을 출력한다.
//================================================


 ▲ 클래스는 기본타입과 일부 참조타입에 대한 다른 이름의 write 메소드들을 가지고 있다.
 write 메소드들을 중복정의 하지 않고, writeBolean(..), writeInt(..), writeLong(..) 과 같은 전혀 다른 시그니처를 갖는다.
 readBolean(), readInt(), readLong() 와 같이 각 write 메소드에 대응되는 이름을 가진 read 메소드를 제공할 수 있다.
 같은 시그니처를 가지면서 리턴 타입만 다른 메소드는 만들 수 없다. 따라서 write 메소드는 중복정의 할 수 있어도 read 메소드는 중복정의가 불가능하다.
 같은 수의 인자를 가진 중복정의 메소드나 생성자가 많더라고, 어떤 메소드가 어떤 인자 집합을 처리하는지 명확하기만 하다면, 프로그래머들은 혼란에 빠지지 않을 것이다.


//================================================
ArrayList 클래스를 이용한 예.
//






















--
void add(Object o) // 주어진 원소를 리스트의 주어진 인덱스에 삽입한다.
boolean addAll(Collection c) // 주어진 Collection 객체 내에 있는 모든 원소를 리스트에 추가한다.
//================================================



Comparable 인터페이스가 등장하기 전까지 compareTo 메소드로 같은 타입을 비교해 왔다.
-> public int compareTo(Sting s);

Comparable 인터페이스 등장후 일반 compareTo메소드도 제공한다.
->public int compareTo(Object o);


compareTo(Object)메소드가 중복정의 메소드 지침을 어기고 있지만, 이 메소드들이 정확하게 똑같이 행동한다면 큰 문제가 되지 않는다.

//================================================
// 중복정의 메소드들 똑같이 행동하기.
//






















--
public int compareTo(Object o) {
return compareTo((String) o);
}

public boolean equals(Object o) {
return o instanceof String && equals((String) o );
}
//================================================


자바 플랫폼 라이브러리의 중복정의 메소드들은 거의 이 장의 지침을 잘 지키고 있다.


//================================================
// String 클래스(중복정의 메소드)
//






















--
static String valueOf (boolean b)
static String valueOf (char c)
static String valueOf (int i)
static String valueOf (long l)
static String valueOf (float f)
static String valueOf (double d)
static String valueOf (object o)
//================================================



Tip - 정리 - 메소드를 중복정의 할 수 있다는 것을 반드시 중복정의 하라는 것을 받아들이지 말아야 한다. - 인자 수가 같은 메소드가 여러개 생기도록 중복정의 하지 말아야 한다. - 기존 클래스가 새로운 인터페이스를 구현하는 것과 같은 상황을 피할 수 없으면, 중복정의 메소드의 행동을 똑같이 만들어야 한다.

*collection : <http://bp2.blogger.com/_pvfHUBL4tpw/R9cwpHfF6rI/AAAAAAAAAFA/CZSpPa4CfIM/s1600-h/java_collection.gif> h2. 항목 27 : 널(null)이 아닌 길이가 0인 (zero-length) 배열을 리턴하라

//================================================
// Null 대신 길이가 0인 배열 리턴하기
//






















--
private List cheesesInStock = ...;
/**

  • 상점에 남아 있는 모든 치즈의 배열을 리턴하거나,
  • 팔 수 있는 치즈가 없으면 null을 리턴한다.
    */
    public Cheese[] getCheese(){
    if(cheeseInStoch.size() == 0)
    return null;
    ....
    }
    //================================================



//================================================
// 간단하게 처리하리
//






















--
if(Arrar.asList(shop.getCheeses()).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing.");
//




















--
// 복잡하게 null처리
//




















--
Cheese[] cheeses = shop.getCheeses();
if(Cheeses !=null &&
Array.asList(shop.getCheeses()).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing.");
//================================================


▲ 길이가 0인 배열을 리턴 할 수 있는 곳에서 null을 리턴하는 메소드를 쓸때 마다, 필요 없는 코드가 반복되어야 한다.

* 배열을 생성, 할당에 드는 비용때문에 길이가 0인 배열보다 null을 리턴할 수 있지만 다음과 같은 이유로 잘못 된 것을 알 수 있다.
** 첫째, 진짜 성능에 문제가 되는 부분이 어디인지 프로파일링하지 않고 성능에 대해 말하는 것은 옳지 않다.
** 둘째, 길이가 0인 배열은 불변 객체이고 불변 객체는 항상 자유롭게 공유할 수 있기 때문에 하나만 만들어 놓으면 모든 메소드에서 쓸 수 있다.


//================================================
// Null 대신 길이가 0인 배열 리턴하기
//






















--
import java.util.*;

class Cheese {
String name;

Chess(String name) {
this.name = name;
}

public static final Chesse STILTON = new Cheese("Stilton");
public static final Chesse CHDDAR = new Cheese("Cheddar");
}

class CheeseShop {
private static Cheese[] ECA = new Chesse[0];
private List cheesesInStock = Collections.singletonList(Cheese.STILTON);

private final static Cheese[] NULL_CHESSE_ARRAY = new Cheese[0];

/**

  • @return Cheese[] 팔 수 있는 모든 치즈의 배열을 리턴한다.
    */
    public Cheese[] getCheeses() {
    return (Cheese[]) cheesesInStock.toArray(NULL_CHEESE_ARRAY);
    }
    }

public class Main {
static CheeseShop shop = new CheeseShop();

public static void main(String[] args) {
Cheese[] cheeses = shop.getCheeses();

if(Arrays.asList(cheese).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing.");

if(!Arrays.asList(cheese).contains(Cheese.CHEDDAR))
System.out.println("Oops, too bad.");
}
}
//================================================



Tip - 정리 - 배열을 리턴하는 메소드에서 null을 리턴할 이유가 전혀없다. null을 리턴할 상황이라면 길이가 0인 배열을 리턴하면 된다.

h2. 항목 28 : 외부에 제공하는 API의 모든 구성요소에 대해 문서화 주석을 달아라 - 모든 API에는 명세문서가 있어야 한다. - 자바 프로그래밍 언어에서는 Javadoc이라는 유틸리티를 제공 하므로 API 문서 작업이 쉬워졌다. - 문서화 주석을 작성하는 규칙은 The Javadoc Tool Home Page[http://java.sun.com/j2se/javadoc/]에 정의되어있다. h4. 1. API를 문서화하려면, 외부에 제공하는 모든 클래스, 인터페이스, 생성자, 메소드, 필드에 문서화 주석을 달아야 한다. h4. 2. 메소드의 문서화 주석은 메소드와 클라이언트 사이의 계약을 간명하게 기술해야 한다. * 메소드가 '무엇을 하는가?'를 설명해야 한다. ** 사전조건(precondition), 사후조건(postcondition), 부작용도 기술해야 한다. ** 부작용: 사후조건을 달성하기 위해 반드시 필요하지도 않으면서, 시스템의 상태를 관찰가능한 정도로 변화시키는 행위

//============================================
// 메소드의 문서화 주석
//




















---
@param : 모든 인자에 대한.
@return : 리턴타입이 void가 아니라면
@throw : 발생할 수 있는 모든 예외에 대한
//============================================




/**

  • this 리스트에서 지정한 위치의 구성요소를 찾아 리턴한다.
  • @param index 리턴할 구성요소의 위치.
  • 이 값은 0이나 양의 정수이며 리스트의 크기보다 크지 않아야 한다.
  • @return this 리스트의 지정한 위치의 구성요소
  • @throw IndexOutOfBoundsException 만약, 인덱스가
  • (<tr>index &lt; 0 || index &gt; = this.size()</tr>) 범위를 벗어날 때 발생한다.
    */
 
 
- 문서화 주석은 HTML 태그와 메타문자로 만든다.
- Javadoc 유틸리티는 문서화 주석을 HTML로 변환한다.
- 가장 많이 쓰이는 태그는 <p>, <code>, <tt>, <pre> 이다. 
- <, >, & 와 같은 HTML 메타문자는 escape문자열로 바꿔야한다.
- "this"가 인스턴스 메소드의 문서화 주석에 쓰이면, 항상 현재 호출되는 메소드를 가진 객체를 의미한다.
- 문서화 주석의 첫 문장은 주석을 붙인 대상에 대한 요약설명(summary description)이 된다.
- HTML 유효성 검사기로 Javadoc이 만들어낸 HTML을 검사해 볼 수 있다.




Tip - 정리 - 문서화 주석은 API를 문서화 할 수 있는 가장 훌륭하고 효율적인 방법이다. - 외부에 제공하는 모든 API에 대해 반드시 문서화 주석을 달아야 한다. - 작성하는 표준규칙을 지며 일관성있는 문서를 만들어야 한다. - HTML 메타문자는 반드시 escape 처리해야한다.

h2. 문서에 대하여 * 최초작성자 : [이현숙] * 최초작성일 : 2008년 04월 11일 * 이 문서는 [HeadFirst Design Patterns|http://book.naver.com/bookdb/book_detail.php?bid=1882446]을 정리한 내용 입니다. * 이 문서는 [오라클클럽|http://www.gurubee.net] [자바 웹개발자 스터디|제3차 자바 웹개발자 스터디] 모임에서 작성하였습니다. * 이 문서를 다른 블로그나 홈페이지에 퍼가실 경우에는 출처를 꼭 밝혀 주시면 고맙겠습니다.~^\^